﻿// <copyright file="FilePathValidator.cs" company="SharpSTS">
// Copyright (c) 2007, 2008 All Right Reserved, http://sharpsts.com/
//
// This source is subject to the Microsoft Permissive License.
// Please see the License.txt file for more information.
// All other rights reserved.
//
// THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY 
// KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
// PARTICULAR PURPOSE.
//
// </copyright>
// <author>Barry Dorrans</author>
// <email>barryd@idunno.org</email>
// <date>2008-06-13</date>
// <summary>Validates a file or directory path for illegal names or exploit paths.</summary>

namespace SharpSTS.CardControl
{
    using System;
    using System.Collections.Generic;
    using System.Globalization;
    using System.IO;

    /// <summary>
    /// A class to validate file paths against injection and directory transversal.
    /// </summary>
    internal static class FilePathValidator
    {
        /// <summary>
        /// Reserved, illegal filenames for DOS devices.
        /// </summary>
        private static readonly List<string> reservedFileNames = new List<string> 
            {
                "CLOCK$",
                "AUX",
                "CON",
                "NUL",
                "PRN"
             };

        /// <summary>
        /// Validates the specified file path.
        /// </summary>
        /// <param name="path">The file path to validate.</param>
        /// <returns><c>true</c> if the specified path is a valid; otherwise, <c>false</c>.</returns>
        public static bool IsValid(string path)
        {
            return (IsFileNameValid(Path.GetFileName(path)) && IsDirectoryValid(Path.GetDirectoryName(path)));
        }

        /// <summary>
        /// Determines whether the specified file name is valid.
        /// </summary>
        /// <param name="filename">The file name to validate.</param>
        /// <returns>
        /// 	<c>true</c> if the specified file name is valid; otherwise <c>false</c>.
        /// </returns>
        private static bool IsFileNameValid(string filename)
        {
            if (string.IsNullOrEmpty(filename))
            {
                return false;
            }

            string trimmedfileName = filename.Trim();

            if (string.IsNullOrEmpty(trimmedfileName))
            {
                return false;
            }

            if (trimmedfileName.IndexOfAny(Path.GetInvalidFileNameChars()) > -1)
            {
                return false;
            }

            if (trimmedfileName.IndexOf(
                                        "$",
                                        StringComparison.OrdinalIgnoreCase) > -1 || 
                                        trimmedfileName.IndexOf(":", StringComparison.OrdinalIgnoreCase) > -1)
            {
                return false;
            }

            string fileNameWithoutExtension =
                Path.GetFileNameWithoutExtension(trimmedfileName).ToUpper(CultureInfo.InvariantCulture);

            if (reservedFileNames.Contains(fileNameWithoutExtension))
            {
                return false;
            }

            if (fileNameWithoutExtension.Length > 3 && (fileNameWithoutExtension.IndexOf(
                                                                                         "COM",
                                                                                         StringComparison.OrdinalIgnoreCase) == 0 ||
                                                        fileNameWithoutExtension.IndexOf(
                                                                                         "LPT",
                                                                                         StringComparison.OrdinalIgnoreCase) == 0))
            {
                int number;
                if (int.TryParse(fileNameWithoutExtension.Substring(3), out number))
                {
                    return false;
                }
            }

            return !string.IsNullOrEmpty(trimmedfileName.Replace(".", ""));
        }

        /// <summary>
        /// Determines whether the specified path is a valid relative directory path.
        /// </summary>
        /// <param name="path">The directory path validate.</param>
        /// <returns>
        /// 	<c>true</c> if the specified path is a valid relative directory path; otherwise, <c>false</c>.
        /// </returns>
        private static bool IsDirectoryValid(string path)
        {
            // Obviously a blank path isn't valid.
            if (string.IsNullOrEmpty(path))
            {
                return true;
            }

            string fixedFilePath = path.Trim();

            if (string.IsNullOrEmpty(fixedFilePath))
            {
                return true;
            }

            // Swap \ for / and vice versa depending on the underlying file system.
            fixedFilePath = Path.DirectorySeparatorChar == '/' ? path.Replace(@"\", fixedFilePath) : path.Replace("/", fixedFilePath);

            if (string.IsNullOrEmpty(fixedFilePath))
            {
                return true;
            }

            // Check we don't have any invalid characters in the path.
            if (fixedFilePath.IndexOfAny(Path.GetInvalidPathChars()) > -1)
            {
                return false;
            }

            // Check for NTFS alternate data streams.
            if (fixedFilePath.IndexOf(":", StringComparison.OrdinalIgnoreCase) > -1)
            {
                return false;
            }

            // Check for directory transversal.
            if (fixedFilePath.IndexOf("..", StringComparison.OrdinalIgnoreCase) > -1)
            {
                return false;
            }

            // Check we don't have an absolute path.
            return !Path.IsPathRooted(fixedFilePath);
        }
    }
}